using System;
using System.Collections.Generic;
using System.Text;
using Castle.DynamicProxy;
using System.Reflection;
using Castle.Core.Interceptor;
using Sio.Mdm.Utils;

namespace Sio.Mdm.Client
{
    /// <summary>
    /// Implementation of class that uniformly returns service
    /// regardelss of server or service type.
    /// </summary>
    public static class ServiceFactory
    {
        #region Fields
        private static Dictionary<Type, ServerInformation> ServersCacheInterface = new Dictionary<Type, ServerInformation>();
        private static Dictionary<string, ServerInformation> ServersCacheName = new Dictionary<string, ServerInformation>();
        private static Dictionary<string, Type> ServiceNameToInterface = new Dictionary<string, Type>();
        private static Dictionary<long, MethodInfo> MethodsCache = new Dictionary<long, MethodInfo>();

        private static object LockByInterface = new object();
        private static object LockByName = new object();
        #endregion

        #region Properties

        #region Public

        #region Servers
        public static IList<ServerInformation> Servers
        {
            get
            {
                return Config.Servers.AsReadOnly();
            }
        }
        #endregion

        #region Interceptors
        private static List<Sio.Mdm.Server.IInvokeInterceptor> _interceptors;
        /// <summary>
        /// Get list of custom interceptors that will be invoked before
        /// acctual call to the server is made. It's probably the best that 
        /// you don't use this ;)
        /// </summary>
        public static List<Sio.Mdm.Server.IInvokeInterceptor> Interceptors
        {
            get
            {
                if (null == _interceptors)
                {
                    _interceptors = new List<Sio.Mdm.Server.IInvokeInterceptor>();
                }

                return _interceptors;
            }
        }
        #endregion

        #endregion

        #region Private

        #region DummyService
        private static DummyService _dummyService;
        /// <summary>
        /// One and only instance of "dummy" service returned
        /// when requested service is not available
        /// </summary>
        private static DummyService DummyService
        {
            get
            {
                if (null == _dummyService)
                {
                    _dummyService = new DummyService();
                }

                return _dummyService;
            }
        }
        #endregion

        #endregion

        #region Internal

        #region ObjectProxy
        private static ProxyGenerator _objectProxy;
        /// <summary>
        /// One and only instance of proxy generator
        /// </summary>
        internal static ProxyGenerator ObjectProxy
        {
            get
            {
                if (null == _objectProxy)
                {
                    _objectProxy = new ProxyGenerator();
                }

                return _objectProxy;
            }
        }
        #endregion

        #endregion
        
        #endregion

        #region Methods

        #region Public

        #region GetService
        /// <summary>
        /// Get service that implements passed interface type
        /// </summary>
        /// <param name="interfaceType">Interface of service to retreive</param>
        /// <returns>Instance of service</returns>
        public static Server.IService GetService(Type interfaceType)
        {
            return FindService(interfaceType);
        }        
        /// <summary>
        /// Get service with specified implementing interface
        /// </summary>
        /// <typeparam name="interfaceType">Type of interfects implementd by service</typeparam>
        /// <returns>Instance fo service casted to passed interface type</returns>
        public static interfaceType GetService<interfaceType>() where interfaceType : Server.IService
        {
            return (interfaceType)GetService(typeof(interfaceType));
        }
        /// <summary>
        /// Get service with specified name casted to passed interface type
        /// </summary>
        /// <typeparam name="interfaceType"></typeparam>
        /// <param name="serviceName"></param>
        /// <returns></returns>
        public static interfaceType GetService<interfaceType>(string serviceName) where interfaceType : Server.IService
        {
            return (interfaceType)GetService(typeof(interfaceType), serviceName);
        }
        /// <summary>
        /// Get service with specified name and specified interface type
        /// </summary>
        /// <param name="interfaceType"></param>
        /// <param name="serviceName"></param>
        /// <returns></returns>
        public static Server.IService GetService(Type interfaceType, string serviceName)
        {
            return FindService(interfaceType, serviceName);
        }
        /// <summary>
        /// Get service by it's name
        /// </summary>
        /// <param name="serviceName">Name of service to retreive</param>
        /// <returns>Instance of service</returns>
        public static Server.IService GetService(string serviceName)
        {
            return FindService(serviceName);
        }
        /// <summary>
        /// Invoke method directly on <see cref="IService"/> instance
        /// </summary>
        /// <param name="serviceName">Name of service as defined in config</param>
        /// <param name="methodName">Name of method to invoke</param>
        /// <param name="genericParams">Type of generic parameters if method is generic othervise null</param>
        /// <param name="args">Arguments to be passed to function. The types and count must match with function or exception will be thrown.</param>
        /// <returns>Result of called function</returns>
        /// <remarks>
        /// This function will throw excpetion if call does not succed. The most common errors
        /// are wrong number of arguments or calling a generic ethod without declaring generic types.
        /// </remarks>
        public static object InvokeServiceMethod(string serviceName, string methodName, Type[] paramTypes, Type[] genericParams, object[] args)
        {
            ServerInformation server = GetCachedServerInfo(serviceName);
            Type interfaceType = null;
            Server.IService service = null;

            try
            {
                service = server.GetService(serviceName, out interfaceType);
                MethodInfo method = FindServiceMethod(interfaceType, methodName, genericParams, paramTypes);

                if (method.IsGenericMethod)
                {
                    method = method.MakeGenericMethod(genericParams);
                }

                if (args == null)
                {
                    args = new object[method.GetParameters().Length];
                }

                return method.Invoke(service, args);
            }
            catch (Exception ex)
            {
                Logger.WriteError("When invoking service function {0}.{1}", serviceName, methodName);
                Logger.WriteException(ex);

                throw new Exception
                    (
                    String.Format
                        (
                        "Error invoking service method {0}.{1}. See inner exception!",
                        serviceName, methodName
                        ),
                    ex
                    );
            }

            throw new Exception("Unknown error invoking service method!");
        }
        public static T InvokeServiceMethod<T>(string serviceName, string methodName, Type[] paramTypes, object[] args)
        {
            return (T)InvokeServiceMethod(serviceName, methodName, paramTypes, new Type[] { typeof(T) }, args);
        }
        public static object InvokeServiceMethodExact(string serviceName, string methodName, Type[] paramTypes, Type[] genericParams, object[] args)
        {
            ServerInformation server = GetCachedServerInfo(serviceName);
            Type interfaceType = null;
            Server.IService service = null;

            try
            {
                service = server.GetService(serviceName, out interfaceType);
                MethodInfo method = FindServiceMethod(interfaceType, methodName, genericParams, paramTypes);

                if (method.IsGenericMethod)
                {
                    method = method.MakeGenericMethod(genericParams);
                }

                if (args == null)
                {
                    args = new object[method.GetParameters().Length];
                }

                return method.Invoke(service, args);
            }
            catch (Exception ex)
            {
                Logger.WriteError("When invoking service function {0}.{1}", serviceName, methodName);
                Logger.WriteException(ex);

                throw new Exception
                    (
                    String.Format
                        (
                        "Error invoking service method {0}.{1}. See inner exception!",
                        serviceName, methodName
                        ),
                    ex
                    );
            }

            throw new Exception("Unknown error invoking service method!");
        }
        #endregion

        #region GetServiceDefaultInterface
        /// <summary>
        /// Get default interface for service with specified name
        /// </summary>
        /// <param name="serviceName">Name of service to get interface for</param>
        /// <returns>Interface type definition or null if no service found</returns>
        public static Type GetServiceDefaultInterface(string serviceName)
        {
            if (!ServiceNameToInterface.ContainsKey(serviceName))
            {
                if(FindService(serviceName) == null)
                {
                    ServiceNameToInterface[serviceName] = null;
                }
            }

            return ServiceNameToInterface[serviceName];
        }
        #endregion

        #endregion

        #region Private

        #region FindService
        private static Server.IService FindService(string name)
        {
            ServerInformation server = GetCachedServerInfo(name);
            Type interfaceType = null;
            Server.IService service = null;

            if (server != null)
            {                
                service = server.GetService(name, out interfaceType);

                if (service != null && !ServiceNameToInterface.ContainsKey(name))
                {
                    ServiceNameToInterface[name] = interfaceType;
                }
            }

            return service == null ?
                ObjectProxy.CreateInterfaceProxyWithTarget(interfaceType, DummyService, DummyService) as Server.IService :
                service;
        }
        private static Server.IService FindService(Type interfaceType)
        {
            ServerInformation server = GetCachedServerInfo(interfaceType);
            Server.IService service = null;

            if (server != null)
            {
                service = server.GetService(interfaceType);
            }

            return service == null?                
                ObjectProxy.CreateInterfaceProxyWithTarget(interfaceType, DummyService, DummyService) as Server.IService:
                service;
        }
        private static Server.IService FindService(Type interfaceType, string name)
        {
            ServerInformation server = GetCachedServerInfo(name);
            Server.IService service = null;

            if (server != null)
            {
                service = server.GetService(interfaceType, name);
            }

            return (
                service == null ?
                ObjectProxy.CreateInterfaceProxyWithTarget(interfaceType, (object) DummyService, DummyService as IInterceptor) :
                service
                ) as Server.IService;
        }
        #endregion

        #region FindServiceMethod
        private static MethodInfo FindServiceMethod(Type serviceType, string methodName, Type[] genericArgs, object[] args)
        {
            List<Type> argumentTypes = new List<Type>();

            foreach (object arg in args)
            {
                if (arg == null)
                {
                    // unknow argument
                    argumentTypes.Add(typeof(void));
                }
                else
                {
                    argumentTypes.Add(arg.GetType());
                }
            }

            return FindServiceMethod(serviceType, methodName, genericArgs, argumentTypes.ToArray());
        }
        private static MethodInfo FindServiceMethod(Type serviceType, string methodName, Type[] genericArgs, Type[] arguments)
        {
            long methodKey = GetServiceMethodKey(serviceType, methodName, genericArgs, arguments);

            if (MethodsCache.ContainsKey(methodKey))
            {
                return MethodsCache[methodKey];
            }

            List<MethodInfo> methods = Helper.FlattenMethods(serviceType);
            List<MethodInfo> foundMethods = new List<MethodInfo>();

            foreach (MethodInfo method in methods)
            {
                if (method.Name != methodName)
                {
                    continue;
                }

                if (genericArgs != null && genericArgs.Length == 0 && method.IsGenericMethod)
                {
                    continue;
                }

                ParameterInfo[] parameters = method.GetParameters();

                if (parameters.Length != arguments.Length)
                {
                    continue;
                }

                int i;

                for (i = 0; i < parameters.Length; i++)
                {
                    if (arguments.GetType() == typeof(void))
                    {
                        continue;
                    }

                    if (parameters[i].ParameterType == arguments[i])
                    {
                        continue;
                    }

                    break;
                }

                if (i == parameters.Length)
                {
                    MethodsCache.Add(methodKey, method); 
                    return method;
                }
            }

            throw new ArgumentException(
                String.Format("There are {0} possible methods to call. Add exact parameter bindings.", foundMethods.Count)
            );
        }
        private static long GetServiceMethodKey(Type serviceType, string methodName, Type[] genericArgs, Type[] arguments)
        {
            StringBuilder builder = new StringBuilder();

            builder.Append(serviceType.ToString());
            builder.Append(methodName);

            if (genericArgs != null)
            {
                builder.Append(genericArgs.Length);
            }

            builder.Append(arguments.Length);

            return builder.GetHashCode();
        }

        #endregion

        #region GetCachedServerInfo
        private static ServerInformation GetCachedServerInfo(Type interfaceType)
        {
            ServerInformation server = null;

            lock (LockByInterface)
            {
                if (ServersCacheInterface.ContainsKey(interfaceType))
                {
                    server = ServersCacheInterface[interfaceType];
                }
                else
                {
                    foreach (ServerInformation serverInfo in Servers)
                    {
                        if (serverInfo.HasService(interfaceType))
                        {
                            server = serverInfo;
                            ServersCacheInterface.Add(interfaceType, server);
                            break;
                        }
                    }
                }
            }

            return server;
        }
        private static ServerInformation GetCachedServerInfo(string serviceName)
        {
            ServerInformation server = null;

            lock (LockByName)
            {
                if (ServersCacheName.ContainsKey(serviceName))
                {
                    server = ServersCacheName[serviceName];
                }
                else
                {
                    foreach (ServerInformation serverInfo in Servers)
                    {
                        if (serverInfo.HasService(serviceName))
                        {
                            server = serverInfo;
                            ServersCacheName.Add(serviceName, server);
                            break;
                        }
                    }
                }
            }

            return server;
        }        
        #endregion

        #endregion

        #endregion
    }
}
